Skip to content

Reuse HTTPS connections, negotiate HTTP/2, add new response fields - #19

Merged
jonathan-kosgei merged 2 commits into
masterfrom
http-reuse-and-new-fields
Nov 3, 2026
Merged

jonathan-kosgei merged 2 commits into
masterfrom
http-reuse-and-new-fields

Conversation

@jonathan-kosgei

@jonathan-kosgei jonathan-kosgei commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • The two Feign proxies behind an IpdataService (the full-model/bulk client and the single-field client) used to be built with their own feign.Client.Default (HttpURLConnection), so every lookup paid for its own connection and TLS handshake. They now share one transport: feign-java11's Http2Client over the JDK java.net.http.HttpClient, which keeps a single connection pool for the lifetime of the service, multiplexes requests over HTTP/2 when the server negotiates it (ALPN) and falls back to HTTP/1.1 keep-alive otherwise.
  • The very same feign.Request.Options instance is handed to Http2Client and to both Feign builders. This pairing is load-bearing: Http2Client 11.10 silently builds a new HttpClient (and so a new connection) for every request whose options differ from the ones it was constructed with. ConnectionReuseTest guards it with non-default timeouts.
  • Builder additions: options(Request.Options), connectTimeout(long, TimeUnit), readTimeout(long, TimeUnit); IpdataServiceBuilder.of(url, key, cacheConfig, logger, client, options) next to the unchanged 5-argument overload; feignClient(...) injection kept, the injected client is shared by all calls.
  • New response fields: is_satellite / is_anycast (IpdataModel.isSatellite() / isAnycast(), primitive booleans that read false when the API omits the key), Blocklist.getFirstSeen() / getLastSeen() / getDaysSeen() (nullable String, String, Integer), IpdataField.IS_SATELLITE / IS_ANYCAST, and the single-field service methods isSatellite(ip) / isAnycast(ip).
  • Tests: ConnectionReuseTest (14 tests) runs through the existing MockIpdataServer, which now records the client socket address and protocol of every request. Sequential full-model, single-field, selected-fields, bulk (POST) and IPv6 calls on one service arrive from the same client port, with default options, with connectTimeout/readTimeout and with an explicit Request.Options; a counting feign.Client injected via feignClient(...) serves all calls of both proxies (plain and caching service); the builder options reach the transport on every call; a default-built service works against the plain-HTTP mock (HTTP/1.1 after the h2c upgrade attempt). NewResponseFieldsTest (9 tests) covers the new fields. No test calls the live API.
  • Version 0.3.0 (pom, scm tag, README install snippet). New compile dependency io.github.openfeign:feign-java11:11.10 (same Feign version as the other modules). Java 11 minimum unchanged. CI already runs on pull requests; no workflow change.

Two logical commits: 3685a51 connection reuse / HTTP/2, 6d1d95c new response fields.

What changes for users

  • Lookups after the first one on a given IpdataService no longer pay a TCP + TLS handshake. Create one service per process (or per API key / endpoint), keep it and reuse it for every call; it is thread-safe. There is nothing to close: the JDK 11 HttpClient has no close method and drops idle connections itself. It exposes no per-instance idle timeout, HTTP/2 ping or TCP keepalive setting, so the JDK defaults apply; the jdk.httpclient.keepalive.timeout system property (1200 s by default) governs idle connections.
  • Timeouts: defaults unchanged (10 s connect, 60 s read, follow redirects), now configurable through the builder. With the default transport both timeouts must be positive (Feign's "0 means infinite" convention is not supported by the JDK client; the convenience setters reject non-positive values).
  • Default transport is now the JDK java.net.http.HttpClient instead of HttpURLConnection: proxies follow the standard https.proxyHost / http.nonProxyHosts properties through ProxySelector.getDefault() as before; TLS uses the default SSLContext (javax.net.ssl.* properties). Code that relied on HttpsURLConnection.setDefaultSSLSocketFactory / setDefaultHostnameVerifier should inject a feignClient (Apache HttpClient via feign-httpclient, OkHttp via feign-okhttp). Response bodies are read fully before decoding (they are small JSON documents). Against a plain http:// endpoint the JDK client attempts an h2c upgrade and falls back to HTTP/1.1 (tested against the mock server).
  • Error handling unchanged: HTTP statuses still surface as RemoteIpdataException / RateLimitException and are never retried; a transport IOException (for example on a pooled connection the server had already closed, before any response byte) is still re-sent by Feign's default Retryer, exactly as before, so no second retry layer was added.
  • Compatibility: additive only; no public symbol renamed or removed; User-Agent (io.ipdata.client.java.<version>) and Accept: application/json unchanged.
  • New data: model.isSatellite(), model.isAnycast(), blocklist.getFirstSeen(), getLastSeen() (YYYY-MM-DD, UTC, kept as strings), getDaysSeen(); getFields(ip, IpdataField.IS_SATELLITE, IpdataField.IS_ANYCAST); ipdataService.isSatellite(ip) / isAnycast(ip). The decoder ignores unknown keys (FAIL_ON_UNKNOWN_PROPERTIES was already false; now covered by a test).

Release notes

ipdata-java-client 0.3.0

Connection reuse and HTTP/2
- One persistent connection pool per IpdataService, shared by the full-model, single-field
  and bulk calls. The default transport is now the JDK HTTP/2 client (feign-java11 over
  java.net.http.HttpClient): requests to api.ipdata.co are multiplexed over HTTP/2 when the
  server negotiates it and fall back to HTTP/1.1 keep-alive otherwise. Create one service per
  API key or endpoint and reuse it; it is thread-safe and has nothing to close.
- Builder: connectTimeout(long, TimeUnit), readTimeout(long, TimeUnit) and
  options(feign.Request.Options). Defaults unchanged: 10 s connect, 60 s read.
- IpdataServiceBuilder.of(url, key, cacheConfig, logger, client, options) overload. The
  5-argument overload and feignClient(...) injection keep working; an injected client now
  serves all calls of the service.
- New dependency io.github.openfeign:feign-java11:11.10. Java 11 minimum unchanged.

New response fields
- is_satellite and is_anycast: IpdataModel.isSatellite() / isAnycast() (false when the API
  omits the key), IpdataField.IS_SATELLITE / IS_ANYCAST for getFields(...), and
  IpdataService.isSatellite(ip) / isAnycast(ip).
- Blocklist entries: getFirstSeen(), getLastSeen() (YYYY-MM-DD strings, UTC) and
  getDaysSeen() (Integer); null when the feed does not report them.

Maven: co.ipdata.client:ipdata-java-client:0.3.0

Tag to push after merge: v0.3.0 (the Release workflow triggers on v* tags and deploys to Maven Central). Note: the existing tags v0.2.1 and v0.2.2 both point at pom version 0.2.1, and Maven Central has no 0.2.2; 0.3.0 is the next published version.

Verification

Local builds with JDK 11, Maven wrapper 3.6.3 and a UTF-8 locale (LC_ALL=C.UTF-8 LANG=C.UTF-8, see "Noticed, not changed"):

  1. Baseline on origin/master (cda239d): ./mvnw -B -Dmaven.artifact.threads=1 clean test post-integration-test → Tests run: 57, Failures: 0, Errors: 0, Skipped: 0, BUILD SUCCESS.
  2. After commit 1 (3685a51): same command → Tests run: 71, Failures: 0, Errors: 0, Skipped: 0 (14 new tests in ConnectionReuseTest), BUILD SUCCESS. The lifecycle runs package, so the main, sources and javadoc jars (ipdata-java-client-0.3.0*.jar) were built; no Javadoc warnings.
  3. After commit 2 (6d1d95c, the pushed HEAD): same command → Tests run: 80, Failures: 0, Errors: 0, Skipped: 0 (9 new tests in NewResponseFieldsTest), BUILD SUCCESS. This is the CI command (mvn clean test post-integration-test, temurin 11).

Two sequential requests from an IpdataService to api.ipdata.co were served over HTTP/2 and the second reused the connection.

CI: the CI workflow (.github/workflows/ci.yml, pull_request trigger for master) ran on this PR for 6d1d95c: run #10, https://github.com/ipdata/java/actions/runs/41621426785, job test success (mvn clean test post-integration-test on temurin 11). No follow-up commits were needed.

Noticed, not changed

  • TestContext.get() / post() and MockIpdataServer.handleBulk() decode HTTP bodies with the platform default charset (new InputStreamReader(stream) without a charset): on JDK 11 with a POSIX locale 10 tests fail on emoji / currency strings, and pass with LC_ALL=C.UTF-8. Pre-existing, test-only.
  • The API now returns languages[].code but the Language model has no code field; the lenient project mapper drops it, the strict test mapper would reject a fixture containing it.
  • Ipv6SafeClient uses the deprecated Request.create(HttpMethod, String, Map, byte[], Charset) (deprecation warning at compile time); it still works with Feign 11.10.
  • README: the "Basic Usage" sample output predates is_vpn, blocklists and scores; the table of contents links "API" to #create-an-instance, an anchor that does not exist; "Bulk data" is a ### heading although it belongs to the API section.
  • IpdataServiceSupport.getSingleFieldClient() returns the full-model client and getApi() the single-field client (names swapped; harmless, both are Lombok delegates).
  • Scores and Blocklist use plain getX() getters while every other model uses fluent accessors (@Accessors(fluent = true)); the new Blocklist getters follow the class they live in.
  • .mvn/extensions.xml declares the polyglot-groovy 0.4.3 build extension although the project uses a plain pom.xml; it is downloaded on every fresh build and never used.
  • GitHub reports one high-severity Dependabot alert on the default branch (alert Doesn't work with URL encoded IPv6 request #10); the dependency set was not touched beyond adding feign-java11.

🤖 Generated with Claude Code

Both Feign proxies behind an IpdataService (the full-model/bulk client
and the single-field client) were built with their own
feign.Client.Default, i.e. HttpURLConnection, so every lookup paid for
its own connection setup and TLS handshake. They now share one
transport: feign-java11's Http2Client over the JDK java.net.http client,
which keeps a single connection pool for the lifetime of the service,
multiplexes requests over HTTP/2 when the server negotiates it and falls
back to HTTP/1.1 keep-alive otherwise.

The same feign.Request.Options instance is handed to Http2Client and to
both Feign builders: Http2Client silently builds a new HttpClient (and
so a new connection) for every request whose options differ from the
ones it was constructed with. Timeouts are configurable through the
builder (connectTimeout/readTimeout or options(Request.Options)); the
existing IpdataServiceBuilder.of(url, key, cacheConfig, logger, client)
factory is kept and an injected feignClient keeps working, shared by all
calls of the service.

Tests: MockIpdataServer records the client socket address and protocol
of every request; ConnectionReuseTest proves that sequential full-model,
single-field, selected-fields and bulk calls arrive on one connection
(with default and custom options), that an injected client serves both
proxies, that the builder options reach the transport on every call and
that the default transport works against the plain-HTTP mock (h2c
upgrade attempt, HTTP/1.1 fallback).

Version 0.3.0 (pom version, scm tag, README install snippet); the README
gains a "Connection reuse and HTTP/2" section.

Claude-Session: https://claude.ai/code/session_01VQiRp4zEJKUXHrKkFaeUke
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The API now flags satellite ISP addresses (is_satellite) and anycast
addresses (is_anycast) next to is_eu, sending each key only when it is
true, and every threat.blocklists entry may carry first_seen and
last_seen (YYYY-MM-DD, UTC) and days_seen; some feeds, such as ipdata's
own vpn feed, omit all three.

IpdataModel gains primitive booleans mapped like is_eu (isSatellite(),
isAnycast()) that read false when the key is absent. Blocklist gains
nullable String firstSeen/lastSeen and Integer daysSeen; the dates stay
strings (no new dependency, no format coupling). IpdataField gains
IS_SATELLITE and IS_ANYCAST for getFields(...), and the single-field
service methods isSatellite(ip) and isAnycast(ip) follow the existing
GET /{ip}/{field} pattern. The Jackson decoder already ignores unknown
properties (FAIL_ON_UNKNOWN_PROPERTIES is false); a test now covers it.

Tests: fixtures built from the API samples (98.97.34.0 satellite ISP,
8.8.8.8 extended with is_anycast and two dated blocklist entries,
187.13.250.1 VPN exit whose third blocklist entry has no dates);
NewResponseFieldsTest asserts the typed values, the false-when-absent
behaviour, the single-field endpoints, field selection with the new
names, bulk responses and unknown-key tolerance, with and without the
cache. The README documents the five fields and the new methods.

Claude-Session: https://claude.ai/code/session_01VQiRp4zEJKUXHrKkFaeUke
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@jonathan-kosgei
jonathan-kosgei merged commit e574984 into master Nov 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants